Room is an Android persistence library that provides an abstraction layer over SQLite. Room makes working with databases relatively easy, even in Kotlin-based Android applications. This detailed lesson will cover:
Sections
- Introduction to Room Database
- Setting Up Room in Your Project
- Creating Entities
- Creating a DAO (Data Access Object)
- Creating the Database
- Using Room in a Repository
- Example Application
- Testing Room Database
1. Introduction to Room Database
Room is a part of Android Jetpack, and it provides an object-mapping layer over SQLite. It basically shields developers from the pain of database management, making possible operations on the database via Kotlin objects. Room provides compile-time checks of SQLite queries, hence preventing runtime errors.
2. Setting Up Room in Your Project
To use Room, you need to add the necessary dependencies to your build.gradle file.
// build.gradle (app level)
dependencies {
def room_version = \"2.5.0\" // Check for the latest version
implementation \"androidx.room:room-runtime:$room_version\"
kapt \"androidx.room:room-compiler:$room_version\" // For Kotlin use kapt
implementation \"androidx.room:room-ktx:$room_version\" // For Kotlin extensions
}
- room-runtime: Te basic library with Room API for database management.
- room-compiler: This is required for compilation of the Room annotations into code at compile time.
- room-ktx: Kotlin-specific extensions for Room make it easier to work with coroutines.
Make sure you apply the Kotlin Kapt plugin at the top of your build.gradle file:
apply plugin: \'kotlin-kapt\'
3. Creating Entities
User Entity
This data class represents a table in the Room database.
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = \"users\") // Specifies the table name
data class User(
@PrimaryKey(autoGenerate = true) val id: Long = 0, // Primary key with auto-increment
val name: String, // User\'s name
val age: Int // User\'s age
)
@Entity:This annotation tells Room that this class is an entity; i.e., it corresponds to a table.tableName: The name of the table in the database.@PrimaryKey: defines the id field as the primary key. The autoGenerate = true means Room will auto-increment this value when a new record is inserted.
4. Creating a DAO (Data Access Object)
UserDao
The DAO interface contains methods that Room uses to interact with the database.
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
@Dao // Annotation indicating this is a DAO
interface UserDao {
@Insert // Indicates that this method will insert a User into the database
suspend fun insertUser(user: User)
@Query(\"SELECT * FROM users\") // SQL query to select all users
suspend fun getAllUsers(): List
@Query(\"SELECT * FROM users WHERE id = :userId\") // SQL query to select a user by ID
suspend fun getUserById(userId: Long): User?
@Query(\"DELETE FROM users\") // SQL query to delete all users
suspend fun deleteAllUsers()
}
@Dao: Marks this interface as a DAO.@Insert: Room will create the necessary code to insert a User in the database.@Query: Allows you to define SQL queries to fetch or manipulate data.
5. Creating the Database
AppDatabase
This abstract class defines the database configuration.
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import android.content.Context
@Database(entities = [User::class], version = 1) // Declares entities and version number
abstract class AppDatabase : RoomDatabase {
abstract fun userDao(): UserDao // Abstract method to access UserDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) { // Thread-safe singleton pattern
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
\"app_database\" // Database name
).build()
INSTANCE = instance
instance
}
}
}
}
@Database: The annotation defines the entities that belong to the database and the version of the database.abstract fun userDao(): UserDao Returns the UserDao that can be used to interact with the DAO methods.- Singleton Pattern: This implementation is performed in such a way that, during the app\’s entire lifetime, just one instance of the database is ever created, thus preventing memory leak and ensuring thread safety.
6. Using Room in a Repository
UserRepository
The repository pattern abstracts the data sources for better separation of concerns.
class UserRepository(private val userDao: UserDao) {
suspend fun insert(user: User) {
userDao.insertUser(user) // Calls the DAO method to insert a user
}
suspend fun getAllUsers(): List<User> {
return userDao.getAllUsers() // Calls the DAO method to get all users
}
suspend fun getUserById(userId: Long): User? {
return userDao.getUserById(userId) // Calls the DAO method to get user by ID
}
suspend fun deleteAllUsers() {
userDao.deleteAllUsers() // Calls the DAO method to delete all users
}
}
UserRepository: This class will provide methods for accessing UserDao, hence encapsulating the data operation that will be done, providing easier testing and maintenance.
7. Example Application
MainActivity
This is where we use the ViewModel to interact with the database.
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private val userViewModel: UserViewModel by viewModels() // ViewModel instance
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Insert a user and fetch all users
lifecycleScope.launch {
userViewModel.insert(User(name = \"Alice\", age = 25)) // Inserts a new user
val users = userViewModel.getAllUsers() // Fetches all users
users.forEach {
println(it) // Prints each user to the console
}
}
}
}
viewModels(): A property delegate that provides an instance of the ViewModel scoped to the activity.lifecycleScope.launch: Launches a coroutine tied to the lifecycle of the activity. This ensures that the coroutine is canceled if the activity is destroyed.
UserViewModel
This ViewModel handles the data for the UI.
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class UserViewModel(private val repository: UserRepository) : ViewModel() {
fun insert(user: User) {
viewModelScope.launch {
repository.insert(user) // Inserts the user using the repository
}
}
suspend fun getAllUsers(): List<User> {
return repository.getAllUsers() // Retrieves all users from the repository
}
}
- ViewModel: It survives configuration changes and is used to store UI-related data.
viewModelScope: A CoroutineScope that is automatically canceled when the ViewModel is cleared.
ViewModel Factory (Optional)
If your ViewModel requires parameters, you need a factory.
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
class UserViewModelFactory(private val repository: UserRepository) : ViewModelProvider.Factory {
override fun create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(UserViewModel::class.java)) {
return UserViewModel(repository) as T // Create a UserViewModel instance
}
throw IllegalArgumentException(\"Unknown ViewModel class\") // Exception for unknown ViewModel
}
}
ViewModelProvider.Factory: This interface is implemented to create ViewModel instances that require parameters.
Using the ViewModel Factory in Activity
class MainActivity : AppCompatActivity() {
private lateinit var userViewModel: UserViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val userDao = AppDatabase.getDatabase(application).userDao() // Get UserDao from the database
val userRepository = UserRepository(userDao) // Create UserRepository
userViewModel = ViewModelProvider(this, UserViewModelFactory(userRepository)).get(UserViewModel::class.java) // Get ViewModel instance
lifecycleScope.launch {
userViewModel.insert(User(name = \"Alice\", age = 25)) // Insert a user
val users = userViewModel.getAllUsers() // Fetch all users
users.forEach {
println(it) // Print users
}
}
}
}
8. Testing Room Database
UserDaoTest
This test class verifies the functionality of the DAO.
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
class UserDaoTest {
private lateinit var database: AppDatabase
private lateinit var userDao: UserDao
@Before
fun setup() {
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
AppDatabase::class.java
).build() // Creates an in-memory database for testing
userDao = database.userDao() // Gets the UserDao
}
@After
fun teardown() {
database.close() // Closes the database after tests
}
@Test
fun testInsertAndGetUser() = runBlocking {
val user = User(name = \"Alice\", age = 25) // Create a test user
userDao.insertUser(user) // Insert the user into the database
val users = userDao.getAllUsers() // Retrieve all users
assertEquals(users.size, 1) // Check that one user exists
assertEquals(users[0].name, \"Alice\") // Check that the user\'s name is correct
}
}
-
JUnit Annotations:
@Before: Runs before each test, setting up the in-memory database.@After: Cleans up by closing the database after each test.@Test: Marks a function as a test case.
-
runBlocking: Runs a coroutine that blocks the current thread, allowing for the testing of suspend functions.
Conclusion
In this lesson, we have gone through the process with a full example of how to implement Room Database in a Kotlin Android application. Every part—starting from Entity and DAO to Repository and ViewModel—plays its role in maintaining data efficiently while keeping the app responsive.
This setup aids not only in managing the data but provides a clear structure by maintaining the principles of separation of concerns and architecture best practices. Any further questions or anything, you would like me to elaborate on, concerning the code above?.